#include "filehandle.h"

#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <assert.h>

/// Calculates x**n, or 1 if n<0.
///
/// The signed n is needed, else the fs_handleIndirect will fail.
STATIC_DEBUG int32_t powi(int32_t x,int32_t n){
	int32_t ret = 1;
	uint32_t i;
	if(n<0) return 1;
	for(i=0;i<(uint32_t)n;++i) ret *=x;
	return ret;
}

/// Get the indirection block and allocate it if needed.
///
/// This works if the function is called at the correct indirection
/// layer. Do not call outside getBlockForCursorPos.
STATIC_DEBUG fs_ret_t fs_handleIndirect(
	FileSystemHandle_t *fsh,
	blockid_t *indirectStartingBlock,
	uint8_t indirectLevel,
	filepos_t cursor,
	blockid_t* blockNeeded,
	byte_t *block,
	bool needToAddBlock)
{
	fs_ret_t ret = RET_NO_PROBLEM;
	uint32_t blockSize = fsh->fsd.blockSize;
	uint32_t blockidPerBlock = blockSize/sizeof(blockid_t);
	blockid_t blockInWhichCursor = (blockid_t)(cursor/(blockSize));
	uint8_t iterIndirect;
	bool toSave = false;// To avoid writing in the first ptr, ie the inode.
	blockid_t *lastBlockId = indirectStartingBlock;
	*blockNeeded = 1337;
	// Going through the indirection levels.
	for(iterIndirect = 0;iterIndirect<=indirectLevel;++iterIndirect){
		uint32_t inCurrentBlockPos = 
			(blockInWhichCursor
				/powi(blockidPerBlock,indirectLevel-1-iterIndirect)
			)%blockidPerBlock;
		if(needToAddBlock){// Need to add block at this level.
			if(blockInWhichCursor%powi(blockidPerBlock,indirectLevel-iterIndirect)==0){
				ret = fs_bookAnyEmptyBlock(fsh,lastBlockId);
				DEBUG_PRINT("+ %d i %d\n",*lastBlockId,iterIndirect)
				if(!ret&&toSave){
					ret = fs_setBlockToHdi(fsh->hdi,*blockNeeded,block);
				}
				if(ret) return ret;
			}
		}
		toSave = true;// If allocating the first block was needed, it is now done.
		*blockNeeded = *lastBlockId;
		ret = fs_getBlockFromHdi(fsh->hdi, *blockNeeded,block);
		lastBlockId = ((blockid_t*)(blockid_t*)block)+inCurrentBlockPos;
		if(ret){
			WARNING_PRINT("An error (%d) occured during an allocation. Bad.\n",ret);
			return ret;
		}
	}
	return ret;
}

/// Set blockNeeded with the block on which the cursor point.
///
/// If the place was not allocated, it allocates it.
/// The method to know if allocation is needed is to compare the
/// cursor position and the file size.
/// Don't play with it once read and write are coded,
/// the allocation is quite subtle.
/// \warning The fact it works is a miracle. Do \b not touch it.
/// or the code around its calls.
STATIC_DEBUG fs_ret_t fs_getBlockForCursorPos(
	FileSystemHandle_t *fsh,
	FileHandle_t* handle,
	filepos_t cursor,
	blockid_t* blockNeeded)
{
	// The block addition is handled inside the block reaching part.
	byte_t *block = malloc(fsh->fsd.blockSize);
	fs_ret_t ret = RET_NO_PROBLEM;
	uint32_t blockSize = fsh->fsd.blockSize;
	uint32_t blockidPerBlock = blockSize/sizeof(blockid_t);
	// This variable is set to the begining of the current area on each test.
	blockid_t blockInWhichCursor = (blockid_t)(cursor/(blockSize));
	bool needToAddBlock = cursor % fsh->fsd.blockSize == 0 && handle->inode.fileSize<=cursor;
	if(!block) return ERR_OUT_OF_MEMORY;
	if(blockInWhichCursor < FS_INODE_STATIC_BLOCKS_COUNT){// No indirect.
		if(needToAddBlock){
			ret = fs_bookAnyEmptyBlock(fsh,handle->inode.blocksfirsts+blockInWhichCursor);
			DEBUG_PRINT("+ %d indirect 0\n",*(handle->inode.blocksfirsts+blockInWhichCursor))
			if(ret){
				goto cleanup;
			}
		}
		*blockNeeded = handle->inode.blocksfirsts[blockInWhichCursor];
		ret = RET_NO_PROBLEM;
		goto cleanup;
	}
	blockInWhichCursor -= FS_INODE_STATIC_BLOCKS_COUNT;
	cursor -= FS_INODE_STATIC_BLOCKS_COUNT*blockSize;
	if(blockInWhichCursor < blockidPerBlock){// First indirect.
		ret = fs_handleIndirect(fsh,&(handle->inode.simpleIndirect),1,
								cursor,blockNeeded,block,needToAddBlock);
		goto cleanup;
	}
	blockInWhichCursor -= blockidPerBlock;
	cursor -= blockidPerBlock*blockSize;
	if(blockInWhichCursor < blockidPerBlock*blockidPerBlock){// Second indirect.
		ret = fs_handleIndirect(fsh,&(handle->inode.doubleIndirect),2,
								cursor,blockNeeded,block,needToAddBlock);
		goto cleanup;
	}
	blockInWhichCursor -= blockidPerBlock*blockidPerBlock;
	cursor -= blockidPerBlock*blockidPerBlock*blockSize;
	if(blockInWhichCursor < blockidPerBlock*blockidPerBlock*blockidPerBlock){// Third indirect.
		ret = fs_handleIndirect(fsh,&(handle->inode.tripleIndirect),3,
								cursor,blockNeeded,block,needToAddBlock);
		goto cleanup;
	}
	cleanup:
	free(block);
	return ret;
}

fs_ret_t fs_newFileHandle(FileSystemHandle_t *fsh, inodeid_t toOpen, filehandleid_t* handleId){
	filehandleid_t i;
	inode_t inode;
	fs_ret_t ret = RET_NO_PROBLEM;
	if(fs_inodeIsOpened(fsh, toOpen, handleId)){
		ret = ERR_ALREADY_DONE;
		goto cleanup;
	}
	for(i = 0;i<fsh->nAllocatedFileHandles;++i){
		if(fsh->openedFilesHandles[i].isEmpty) break;
	}
	if(i>=fsh->nAllocatedFileHandles){
		// TODO : handle dynamical resizing of the array.
		*handleId = 0xFFFF;
		ret = ERR_OUT_OF_RESSOURCE;
		goto cleanup;
	}
	*handleId = i;
	ret = fs_getInode(fsh,toOpen,&inode);
	if(ret) goto cleanup;
	fsh->openedFilesHandles[i].isEmpty = false;
	fsh->openedFilesHandles[i].isReadOnly = false;
	fsh->openedFilesHandles[i].isWriteOnly = false;
	fsh->openedFilesHandles[i].fileId = toOpen;
	fsh->openedFilesHandles[i].inode = inode;
	fsh->openedFilesHandles[i].cursor = 0;
	
	cleanup:
	return ret;
}

fs_ret_t getFileHandle(FileSystemHandle_t *fsh,filehandleid_t handleId,FileHandle_t **handle){
	if(handleId>=fsh->nAllocatedFileHandles){
		*handle = NULL;
		return ERR_PARAMETER_PROBLEM;
	}
	if(fsh->openedFilesHandles[handleId].isEmpty){
		*handle = NULL;
		return ERR_PARAMETER_PROBLEM;
	}
	*handle = fsh->openedFilesHandles+handleId;
	return RET_NO_PROBLEM;
}

fs_ret_t fs_deleteFileHandle(FileSystemHandle_t *fsh, filehandleid_t handleId){
	byte_t *block = malloc(fsh->fsd.blockSize);
	fs_ret_t ret = RET_NO_PROBLEM;
	FileHandle_t *handle = NULL;
	if(!block) return ERR_OUT_OF_MEMORY;
	ret = getFileHandle(fsh,handleId,&handle);
	if(ret) goto cleanup;
	handle->inode.lastAccess = time(NULL);
	memcpy(block,&(handle->inode),sizeof(inode_t));
	fs_setBlockToHdi(fsh->hdi,handle->fileId, block);
	handle->isEmpty = true;
	cleanup:
	free(block);
	return ret;
}

fs_ret_t fs_inodeIsOpened(FileSystemHandle_t *fsh, inodeid_t toCheck, filehandleid_t* handleId){
	filehandleid_t i;
	for(i = 0;i<fsh->nAllocatedFileHandles;++i){
		if(fsh->openedFilesHandles[i].fileId==toCheck
			&&(!fsh->openedFilesHandles[i].isEmpty)){
			if(handleId){
				*handleId = i;
			}
			return RET_TRUE;
		}
	}
	return RET_FALSE;
}

fs_ret_t fs_d_writeByte(FileSystemHandle_t *fsh, filehandleid_t handleId,filepos_t pos,byte_t byte){
	blockid_t blockNeeded;
	byte_t *block = malloc(fsh->fsd.blockSize);
	fs_ret_t ret = RET_NO_PROBLEM;
	if(!block) return ERR_OUT_OF_MEMORY;
	ret = fs_getBlockForCursorPos(fsh, &(fsh->openedFilesHandles[handleId]), pos , &blockNeeded);
	if(ret) goto cleanup;
	ret = fs_getBlockFromHdi(fsh->hdi,blockNeeded,block);
	if(ret) goto cleanup;
	block[pos%fsh->fsd.blockSize] = byte;
	ret = fs_setBlockToHdi(fsh->hdi,blockNeeded,block);
	if(ret) goto cleanup;
	++(fsh->openedFilesHandles[handleId].inode.fileSize);
	cleanup:
	free(block);
	return RET_NO_PROBLEM;
}

fs_ret_t fs_seek(FileSystemHandle_t *fsh, filehandleid_t handleId,enum Seek_rel_t fromWhere, filepos_t pos){
	filepos_t finalPos;
	if(fromWhere==FS_SEEK_CUR){
		finalPos = fsh->openedFilesHandles[handleId].cursor;
	}else if(fromWhere==FS_SEEK_END){
		finalPos = fsh->openedFilesHandles[handleId].inode.fileSize;
	}else if(fromWhere==FS_SEEK_SET){
		finalPos = 0;
	}else{
		return ERR_PARAMETER_PROBLEM;
	}
	finalPos +=pos;
	if(finalPos<0||finalPos>fsh->openedFilesHandles[handleId].inode.fileSize){
		return ERR_OUT_OF_RANGE;
	}
	fsh->openedFilesHandles[handleId].cursor = finalPos;
	return RET_NO_PROBLEM;
}

fs_ret_t fs_tell(FileSystemHandle_t *fsh, filehandleid_t handleId,filepos_t* pos){
	*pos = fsh->openedFilesHandles[handleId].cursor;
	return RET_NO_PROBLEM;
}

fs_ret_t fs_getSizeFromFileHandle(FileSystemHandle_t *fsh, filehandleid_t handleId, filepos_t *size){
	FileHandle_t *handle = NULL;
	fs_ret_t ret = getFileHandle(fsh,handleId,&handle);
	if(ret) return ret;
	*size = handle->inode.fileSize;
	return RET_NO_PROBLEM;
}

fs_ret_t fs_write(FileSystemHandle_t *fsh, filehandleid_t handleId, const byte_t* data, uint32_t dataSize){
	// Be careful editing this function. Any operation consisting to write
	// several times the same block \b will cause problems (reallocation
	// of an already allocated block) if done after the file end.
	// This is due to the way fs_getBlockForCursorPos works.
	FileHandle_t *handle = NULL;
	fs_ret_t ret = RET_NO_PROBLEM;
	blockid_t currentBlockId = 0;
	byte_t *block = malloc(fsh->fsd.blockSize);
	if(!block){
		ret = ERR_OUT_OF_MEMORY;
		goto cleanup;
	}
	if(dataSize==0) goto cleanup;
	ret = getFileHandle(fsh,handleId,&handle);
	if(ret) goto cleanup;

	if(handle->isReadOnly){
		WARNING_PRINT("Cannot write in a read-only file.\n");
		return ERR_FORBIDDEN;
	}
	
	while(dataSize!=0){
		uint32_t sizeToWrite = 0;
		ret = fs_getBlockForCursorPos(fsh, handle, handle->cursor, &currentBlockId);
		if(ret) goto cleanup;
		//DEBUG_PRINT("Gonna write %d\n",currentBlockId)
		if(handle->cursor%fsh->fsd.blockSize==0){
			if(dataSize<fsh->fsd.blockSize){// The end.
				ret = fs_getBlockFromHdi(fsh->hdi,currentBlockId,block);
				if(ret) goto cleanup;
				sizeToWrite = dataSize;
			}else{// The middle.
				sizeToWrite = fsh->fsd.blockSize;
			}
			memcpy(block,data,sizeToWrite);
		}else{// The beginning.
			ret = fs_getBlockFromHdi(fsh->hdi,currentBlockId,block);
			if(ret) goto cleanup;
			// The real one.
			if(dataSize > fsh->fsd.blockSize-(handle->cursor%fsh->fsd.blockSize)){
				sizeToWrite = fsh->fsd.blockSize-(handle->cursor%fsh->fsd.blockSize);
			}else{// Also the end.
				sizeToWrite = dataSize;
			}
			memcpy(block+(handle->cursor%fsh->fsd.blockSize),data,sizeToWrite);
		}
		ret = fs_setBlockToHdi(fsh->hdi,currentBlockId,block);
		if(ret) goto cleanup;
		data += sizeToWrite;
		handle->cursor += sizeToWrite;
		dataSize -= sizeToWrite;
	}
	if(handle->cursor > handle->inode.fileSize){
		// If we finished after the file size, need to update it.
		handle->inode.fileSize = handle->cursor;
	}
	cleanup:
	free(block);
	return ret;
}

fs_ret_t fs_read(FileSystemHandle_t *fsh, filehandleid_t handleId, byte_t* data, uint32_t dataSize){
	FileHandle_t *handle = NULL;
	fs_ret_t ret = RET_NO_PROBLEM;
	blockid_t currentBlockId = 0;
	byte_t *block = malloc(fsh->fsd.blockSize);
	if(!block){
		ret = ERR_OUT_OF_MEMORY;
		goto cleanup;
	}
	if(dataSize==0){
		DEBUG_PRINT("Read : nothing to do.\n");
		goto cleanup;
	}
	ret = getFileHandle(fsh,handleId,&handle);
	if(ret) goto cleanup;
	if(handle->isWriteOnly){
		WARNING_PRINT("Cannot read in a write-only file.\n");
		return ERR_FORBIDDEN;
	}
	if(handle->cursor >= handle->inode.fileSize){
		ret = ERR_OUT_OF_RANGE;
		goto cleanup;
	}
	if(dataSize+handle->cursor > handle->inode.fileSize){
		dataSize = (uint32_t)(handle->inode.fileSize-handle->cursor);
	}
	
	while(dataSize!=0){
		uint32_t sizeToRead = 0;
		ret = fs_getBlockForCursorPos(fsh, handle, handle->cursor, &currentBlockId);
		if(ret) goto cleanup;
		//DEBUG_PRINT("Gonna read %d, pos %d\n",currentBlockId,(int)handle->cursor);
		ret = fs_getBlockFromHdi(fsh->hdi,currentBlockId,block);
		if(ret) goto cleanup;
		if(handle->cursor%fsh->fsd.blockSize==0){
			if(dataSize<fsh->fsd.blockSize){// The end.
				sizeToRead = dataSize;
			}else{// The middle.
				sizeToRead = fsh->fsd.blockSize;
			}
			memcpy(data,block,sizeToRead);
		}else{// The begining.
			// The real one.
			if(dataSize > fsh->fsd.blockSize - (handle->cursor%fsh->fsd.blockSize)){
				sizeToRead = fsh->fsd.blockSize - (handle->cursor%fsh->fsd.blockSize);
			}else{// Also the end.
				sizeToRead = dataSize;
			}
			memcpy(data,block+(handle->cursor%fsh->fsd.blockSize),sizeToRead);
		}
		data += sizeToRead;
		handle->cursor += sizeToRead;
		dataSize -= sizeToRead;
	}
	cleanup:
	free(block);
	return ret;
}

fs_ret_t fs_emptyItemwithHandle(FileSystemHandle_t *fsh, filehandleid_t handleId){
	fs_ret_t ret = RET_NO_PROBLEM;
	FileHandle_t *handle = NULL;
	ret = getFileHandle(fsh,handleId,&handle);
	if(ret) return ret;
	ret = fs_deleteFileBlocks(fsh,&(handle->inode));
	if(ret) return ret;
	handle->inode.fileSize = 0;
	return ret;
}
